<?php
/**
 * PHBMail - PHBMail.php
 * @version: 1.5.0
 * @date: 2009-08-18
 * @desc: PHBMail SMTP邮件发送类 for PHP5
 * @author: 银魂 <xwsoul@gmail.com>
 * @copyright: Copyright (c) 2007-2009 PHBChina
 * @link:http://code.google.com/p/phbchina/
 */

//设定异常未捕获 callback
function exception_handler($exception)
{
	echo '<div style="font-size:14px;color:#F06;font-family:Comic Sans MS,Courier;">'.$exception.'</div>';
}

set_exception_handler('exception_handler');

//PHBMailException PHBMail异常类
class PHBMailException extends Exception
{
	protected $step;
    public function __construct($step,$message, $code = 0)
    {
        parent::__construct($message, $code);
        $this->step = $step;
    }
    public function __toString() {
    	return '<b>'.__CLASS__.'</b><br /><b>Error Step:</b> '.$this->step.'<br /><b>Error Code:</b> '.$this->code.'<br /><b>Error Desc:</b> '.$this->message.'<br />';
    }
}

class PHBMail{
	//smtp主机地址
	protected $host;
	//用户名
	protected $user;
	//密码
	protected $psw;
	//主机端口
	protected $port=25;
	//主机端口
	protected $timeout;
	//发件人地址
	protected $from;
	//发件人昵称
	protected $sender;
	//是否允许SSL 
	protected $enableSSL=false;
	//是否允许调试 
	protected $debug=false;

	//普通收件人地址
	protected $to;
	protected $toStr;
	//抄送收件人地址
	protected $cc;
	protected $ccStr;
	//可以发送否(发件确认)
	protected $bcc;
	protected $couldSend=false;

	//信件标题
	protected $subject;
	//信件内容
	protected $content;
	//命令确认符
	protected $CRLF;
	//邮件等级 h - high	n - normal	l - low
	protected $priority='n';
	//HTML支持
	public $HTMLsupport=true;
	//字符集
	public $charset='utf-8';
	//是否需要回执
	protected $returnReceipt=false;
	//是否开启64进制转换
	protected $ifBaseChar = true;

	//附件,字符串
	protected $attachment=array();
	//错误号
	protected $errno;
	//错误描述
	protected $error;
	//错误的收件人
	protected $errorAddr;
	//错误步骤
	protected $step;
	//SMTP链接
	protected $fp;


	/**
	 * 构造
	 */
	public function __construct($host,$user,$psw,$port=25,$CRLF="\n", $timeout=1)
	{
		$this->host = $host;
		$this->port = $port;
		$this->user = $user;
		$this->psw = $psw;
		$this->CRLF = $CRLF;
		$this->timeout = $timeout*1000000;
	}

	/**
	 * 析构
	 */
	public function __destruct()
	{
		$this->disconnect();
	}
	
	public function enableDebug($bool)
	{
		$this->debug = $bool;
	}

	public function enableSSL($bool)
	{
		$this->enableSSL = $bool;
	}
	
	/**
	 * 连接服务器
	 */
	protected function connect()
	{
		error_reporting(1);
//		if ($this->enableSSL)
//			$this->host = 'ssl://'.$this->host;
		$this->fp = fsockopen($this->host, $this->port, $this->errno, $this->error, 5);
		error_reporting(7);
		if (!$this->fp)
		{
			$this->makeInfo('Server Connect !','Can not connect to the server --- ['.$this->host.']! Please check it !',403);
			$this->throwE();
		}
		//设置流资源的超时时间 - 1秒!避免fgets函数操作被挂起,导致脚本超时!
		stream_set_timeout($this->fp,null,$this->timeout);
		$this->smtpInfo('Waitting for connection !',220);
		$this->sayHello();
		if($this->enableSSL)
		{
			$this->startTLS();
		}
		$this->readytoLogin();
		$this->checkUser();
		$this->checkPass();
		return true;
	}

	/**
	 * 断开连接
	 */
	protected function disconnect()
	{
		error_reporting(1);
		$this->smtpWrite('QUIT');
		fclose($this->fp);
		error_reporting(7);
	}
	
	private function startTLS()
	{
		$this->smtpWrite('STARTTLS');
		$this->smtpInfo('StartTLS',220);
		stream_socket_enable_crypto($this->fp, true,STREAM_CRYPTO_METHOD_SSLv3_CLIENT);
	}

	/**
	 * 向服务器打招呼
	 */
	private function sayHello()
	{
		$this->smtpWrite('helo '.$this->host);
		$this->smtpInfo('Say hello',250);
		return true;
	}

	/**
	 * 准备登陆
	 */
	private function readytoLogin()
	{
		$this->smtpWrite('auth login');
		$this->smtpInfo('Prepare to login',334);
		return true;
	}

	/**
	 * 验证用户名 用户名会以64位编码后传送
	 */
	private function checkUser()
	{
		$this->smtpWrite(base64_encode($this->user));
		$this->smtpInfo('Check Username',334);
		return true;
	}

	/**
	 * 验证密码 用户名会以64位编码后传送
	 */
	private function checkPass()
	{
		$this->smtpWrite(base64_encode($this->psw));
		$this->smtpInfo('Check user password',235);
		return true;
	}

	/**
	 * 邮件发送
	 */
	public function send($subject,$content)
	{
		$this->connect();
		$this->subject = $subject;
		$this->content = $content;
		$this->makeFromAddr();
		$this->makeToAddr();
		$this->dataCommand();
		$this->writeMail();
		if($this->enableSSL)
			stream_socket_enable_crypto($this->fp, false);
		return true;
	}
	
	/**
	 * 收件人信息设置
	 */
	public function setFrom($name,$addr)
	{
		$this->sender = $name;
		$this->from = $addr;
	}

	/**
	 * 写信人地址确认指令
	 */
	private function makeFromAddr()
	{
		if(!$this->sender || !$this->from)
			$this->throwInfo('Check the Email address of the sender !','Use PHBMail::fromer() first !',500);
		$this->smtpWrite('MAIL FROM:<'.$this->from.'>');
		$this->smtpInfo('Check the Email address of the sender',250);
		return true;
	}
	
	/**
	 * 收信人地址确认
	 */
	protected function makeToAddr()
	{
		$this->to();
		$this->cc();
		$this->bcc();
		if(!$this->couldSend)
		{
			$this->error .= $this->errorAddr;
			$this->throwE();
		}
		return true;
	}

	/**
	 * rcpt指令
	 */
	protected function rcpt($email)
	{
		$this->smtpWrite("rcpt to: <{$email}>");
	}
	
	/**
	 * 设置普通收件人信息
	 *
	 * @param array $to
	 */
	public function setTo($to=NULL)
	{
		if(!is_array($to) && empty($to))
			$this->throwInfo('who do you want to send?','Error paramer',500);
		else
			$this->to = $to;
	}
	/**
	 * 生成普通收件人信息
	 *
	 */
	protected function to(){
		if(!$this->to)
			return ;
		foreach($this->to as $name => $email)
		{
			$this->rcpt($email);
			$this->smtpInfoBase('Check the Email addr of the toReceiver - '.$email);
			if($this->errno!==250)
			{
				$this->errorAddr .= "Bad Email address : <b>".$email."</b> !<br />\n";
			}
			else
			{
				$this->toStr .= '"'.$this->baseChar($name).'" <'.$email.'>, '; 
				$this->couldSend = true;
			}
		}
	}
	
	/**
	 * 设置抄送人信息
	 *
	 * @param arr $cc
	 */
	public function setCc($cc=NULL){
		if(!is_array($cc) && empty($cc))
			$this->throwInfo('who do you want to cc?','Error paramer',500);
		else
			$this->cc = $cc;
	}
	/**
	 * 生成抄送收件人信息
	 *
	 */
	protected function cc(){
		if(!$this->cc)
			return ;
		foreach($this->cc as $name => $email)
		{
			$this->rcpt($email);
			$this->smtpInfoBase('Check the Email addr of the ccReceiver - '.$email);
			if($this->errno!==250)
			{
				$this->errorAddr .= "Bad Email address : <b>".$email."</b> !<br />\n";
			}
			else
			{
				$this->ccStr .= '"'.$this->baseChar($name).'" <'.$email.'>, '; 
				$this->couldSend = true;
			}
		}
	}
	
	/**
	 * 设置密送人信息
	 *
	 * @param array $bcc
	 */
	public function setBcc($bcc=NULL)
	{
		if(!is_array($bcc) && empty($bcc))
			$this->throwInfo('who do you want to bcc?','Error paramer',500);
		else
			$this->bcc = $bcc;
	}
	/**
	 * 生成密送信息
	 *
	 */
	protected function bcc(){
		if(!$this->bcc)
			return ;
		foreach($this->bcc as $email)
		{
			$this->rcpt($email);
			$this->smtpInfoBase('Check the Email addr of the ccReceiver - '.$email);
			if($this->errno!==250)
			{
				$this->errorAddr .= "Bad Email address : <b>".$email."</b> !<br />\n";
			}
			else
			{
				$this->couldSend = true;
			}
		}
	}
	
	/**
	 * 写邮件准备指令
	 */
	protected function dataCommand()
	{
		$this->smtpWrite('data');
		$this->smtpInfo('Input command',354);
		return true;
	}

	/**
	 * 是否需要回执
	 */
	public function needReturnReceipt($bool=false)
	{
		$this->returnReceipt = $bool;
	}

	/**
	 * 是否开启字符串64位编码
	 */
	public function ifBaseChar($bool=true)
	{
		$this->ifBaseChar = $bool;
	}

	/**
	 * 设置邮优先级
	 */
	public function setLevel($level='n')
	{
		$this->priority = $level;
	}

	/**
	 * 是否支持HTML
	 *
	 * @param bool $bool
	 */
	public function supportHTML($bool=true)
	{
		$this->HTMLsupport = $bool;
	}
	
	/**
	 * 设置字符集编码
	 *
	 * @param str $char
	 */
	public function setChar($char='GBK')
	{
		$this->charset = $char;
	}
	
	/**
	 * 转换字符
	 *
	 * @param str $Char
	 * @return str
	 */
	protected function baseChar($Char)
	{
		if($this->ifBaseChar===true)
		{
			return '=?'.$this->charset.'?B?'.base64_encode($Char).'?=';
		}
		else
		{
			return $Char;
		}
	}
	
	/**
	 * 写信件
	 */
	protected function writeMail()
	{
		//当前时间
		$time = date('D, j M Y G:i:s');
		//分隔符
		$cutter = uniqid('-=NextMailPart');
		//邮件优先级
		$pri = array('h'=>1,'n'=>3,'l'=>5);
		
		//发件人信息
		$fromInfo = '"'.$this->baseChar($this->sender).'" <'.$this->from.'>';
		$header.= 'From: '.$fromInfo.$this->CRLF;

		//普通发送
		if($this->toStr)
			$header.= 'To: '.$this->toStr.$this->CRLF;
		//抄送
		if($this->ccStr)
			$header.= 'Cc: '.$this->ccStr.$this->CRLF;

		//发送日期
		$header.= "Date: $time +0800{$this->CRLF}";
		//邮件主题
		$header.= "Subject: ".$this->baseChar($this->subject).$this->CRLF;
		//邮件优先级
		$header.= "X-Priority: {$pri[$this->priority]}{$this->CRLF}";
		//是否需要回执
		if($this->returnReceipt)
			$header.= "Disposition-Notification-To: ".$fromInfo.$this->CRLF;
		//MIME 版本
		$header.= "MIME-Version: 1.0".$this->CRLF;
		//主题内容附件等的切割
		$header.= 'Content-Type: Multipart/Mixed; Boundary="'.$cutter.'"'.$this->CRLF.$this->CRLF;
		$cutter = '--'.$cutter;
		$header.= $cutter.$this->CRLF;
		if(!$this->HTMLsupport)
		{
			$header.= "Content-Type: text/plain;charset=".$this->charset.";{$this->CRLF}";
		}
		else
		{
			$header.= "Content-Type: text/html;charset=".$this->charset.";{$this->CRLF}";
		}

		$mail = $header."{$this->CRLF}".$this->content."{$this->CRLF}";
		if(!empty($this->attachment))
		{
			foreach($this->attachment as $attachment)
			{
				$mail .= $cutter.$this->CRLF.$attachment;
			}
			unset($this->attachment);
		}

		$mail .= $cutter.'--';
		$mail .= $this->CRLF.'.';
//		die('<PRE>'.$mail.'</PRE>');
		$this->smtpWrite($mail);
		$this->smtpInfo('Make mail !',250);
		return true;
	}

	/**
	 * 添加附件
	 */
	public function addAttachment($filename)
	{
		if(!file_exists($filename))
		{
			$this->throwInfo('Add attachment !',$filename.' does not exist !',500);
		}
		else
		{
			$basename = basename($filename);
		}
		$headers .= 'Content-Type: '.$this->attachmentMime($basename).';'.$this->CRLF;
		$headers .= 'Content-Transfer-Encoding: base64'.$this->CRLF;
		$headers .= 'Content-Disposition: attachment;filename="'.$basename.'"'.$this->CRLF.$this->CRLF;
		$file = $this->attachmentEncode($filename);
		$this->attachment[] = $headers.$file;
	}

	/**
	 * 返回附件Mime名
	 */
	protected function attachmentMime($filename)
	{
		$mime = array(
			'php'=>'text/plain',
			'txt'=>'text/plain',
			'asc'=>'text/plain',
			'html'=>'text/plain',
			'htm'=>'text/plain',
			'js'=>'text/plain',
			'css'=>'text/plain',
			'asp'=>'text/plain',
			'java'=>'text/plain',
			'xml'=>'text/plain',
			'xsl'=>'text/plain',
			'wml'=>'text/plain',
			'wmls'=>'text/plain',
			'doc'=>'application/msword',
			'xls'=>'application/vnd.ms-excel',
			'ppt'=>'application/vnd.ms-powerpoint',
			'pdf'=>'application/pdf',
			'rar'=>'application/rar',
			'zip'=>'application/zip',
			'tar'=>'application/x-tar',
			'gtar'=>'application/x-gtar',
			'gz'=>'application/x-gzip',
			'mid'=>'audio/midi',
			'midi'=>'audio/midi',
			'mp3'=>'audio/mpeg',
			'ram'=>'audio/x-pn-realaudio',
			'ra'=>'audio/x-pn-realaudio',
			'rm'=>'application/vnd.rn-realmedia',
			'rmvb'=>'application/vnd.rn-realmedia',
			'wav'=>'audio/x-wav',
			'bmp'=>'image/bmp',
			'gif'=>'image/gif',
			'jpg'=>'image/jpeg',
			'jpeg'=>'image/jpeg',
			'jpe'=>'image/jpeg',
			'png'=>'image/png',
			'ico'=>'image/x-icon',
			'avi'=>'video/x-msvideo'
		);
		$filename = explode('.',$filename);
		$extname = strtolower($filename[count($filename)-1]);
		if(empty($mime[$extname]))
		{
			return 'application/octet-stream';
		}
		else
		{
			return $mime[$extname];
		}

	}

	/**
	 * 对附件进行64位编码 - 发送附件必须
	 */
	protected function attachmentEncode($filename)
	{
		$handle = fopen($filename,'r+');
		$fContent = fread($handle,filesize($filename));
		fclose($handle);
		return chunk_split(base64_encode($fContent));
	}

	/**
	 * 从服务器读取smtp信息
	 *
	 * @param str $step
	 */
	protected function smtpInfoBase($step)
	{
		$content = fgets($this->fp, 200);
		if($this->debug)
			echo '[<b>PHBMail Trace</b>] - (', date('H:i:'), date('s')+round(microtime(),4), ')<br />[Step]: ', $step, '<br />&nbsp;&nbsp;&nbsp;&nbsp;[Response]: ', $content, "<br /><br />\n";
		$this->makeInfo($step,substr($content,4,200),intval(substr($content,0,3)));
	}
	protected function smtpInfo($step,$correctCode)
	{
		$this->smtpInfoBase($step);
		if($this->errno!==$correctCode)
			$this->throwE();
	}
	protected function smtpWrite($content)
	{
		if($this->debug)
			echo '[Send] : ',htmlspecialchars($content),"<br />\n";
		fwrite($this->fp,$content.$this->CRLF);
	}

	/**
	 * 生成错误报告
	 *
	 * @param str $step
	 * @param str $error
	 * @param int $errno
	 */
	protected function makeInfo($step,$error,$errno)
	{
		$this->errno = $errno;
		$this->error = $error;
		$this->step = $step;
	}
	
	/**
	 * PHBMail 抛出异常
	 *
	 */
	protected function throwE()
	{
		$this->disconnect();
		throw new PHBMailException($this->step,$this->error,$this->errno);
	}
	
	/**
	 * 直接给出信息抛出
	 *
	 * @param str $step
	 * @param str $error
	 * @param int $errno
	 */
	protected function throwInfo($step,$error,$errno)
	{
		$this->makeInfo($step,$error,$errno);
		$this->throwE($step,$error,$errno);
	}
}